package com.jinxinol.core.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.NodeIterator;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/**
 * The XMLObject class encapsulates some common operation used when
 * processing XML DOM tree and converting between XML codes and real
 * objects. 
 */
public class XMLObject {
	
	private static final Log logger = LogFactory.getLog(XMLObject.class);
	
	private Node root = null;

	/**
	 * Construct a DOM tree with a root node
	 */
	public XMLObject(String rootName, boolean isXpath) {
		try {
			DocumentBuilderFactory dfactory =
				DocumentBuilderFactory.newInstance();
			dfactory.setNamespaceAware(false);
			dfactory.setIgnoringElementContentWhitespace(false);
			Document doc = dfactory.newDocumentBuilder().newDocument();
			root = doc.createElement(rootName);
			doc.appendChild(root);
		}
		catch (FactoryConfigurationError e) {
			e.printStackTrace();
		}
		catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * This constructor takes a string parameter as the source of the DOM tree.
	 * @param xmlBlock The xml source
	 */
	public XMLObject(String xmlBlock) {
		StringReader sr = new StringReader(xmlBlock);

		try {
			// Set up a DOM tree to query.
			InputSource in = new InputSource(sr);
			DocumentBuilderFactory dfactory =
				DocumentBuilderFactory.newInstance();
			dfactory.setNamespaceAware(false);
			dfactory.setIgnoringElementContentWhitespace(false);
			Document doc = dfactory.newDocumentBuilder().parse(in);
			root = doc.getFirstChild();
		} catch (IOException ie) {
			ie.printStackTrace();
		} catch (SAXException se) {
			se.printStackTrace();
		} catch (ParserConfigurationException pe) {
			pe.printStackTrace();
		}
	}

	/**
	 * This constructor takes an InputStream parameter as the source of the DOM tree.
	 * The constructor reads string source from the InputStream.
	 * @param is The InputStream object which contains the xml source
	 */
	public XMLObject(InputStream is) {
		try {
			// Set up a DOM tree to query.
			InputSource in = new InputSource(is);
			DocumentBuilderFactory dfactory =
				DocumentBuilderFactory.newInstance();
			dfactory.setNamespaceAware(false);
			dfactory.setIgnoringElementContentWhitespace(true);
			dfactory.setValidating(false);
			Document doc = dfactory.newDocumentBuilder().parse(in);
			root = doc.getFirstChild();
		} catch (IOException ie) {
			ie.printStackTrace();
		} catch (SAXException se) {
			se.printStackTrace();
		} catch (ParserConfigurationException pe) {
			pe.printStackTrace();
		}
	}
	
	/**
	 * Construct an XMLObject instance according to a node of a DOM TreeOld.
	 * @param node
	 */
	public XMLObject(Node node) {
		root = node;
	}

	/**
	 * Check whether the node is a text node
	 * @param node The node being tested
	 * @return True if the node is a text node
	 */
	private boolean isTextNode(Node node) {
		if (node == null)
			return false;
		short nodeType = node.getNodeType();
		return nodeType == Node.CDATA_SECTION_NODE
			|| nodeType == Node.TEXT_NODE;
	}

	/**
	 * Get the Root Node of this XML Object
	 * @return The root node
	 */
	public Node getRootNode() {
		return root;
	}

	/**
	 * Get the root element name of this XMLObject
	 * @return The name of the root element in this xml stream
	 */
	public String getRootElement() {
		return root.getNodeName();
	}

	/**
	 * Check whether the indicated element(s) exist
	 * @param xpath The path of the element being checked
	 * @return true if the element(s) exist
	 */
	public boolean hasElement(String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			return (nl != null) && (nl.nextNode() != null);
		} catch (TransformerException te) {
		}
		return false;
	}

	/**
	 * Returns the Document object associated with the given node
	 * @param node
	 * @return
	 */
	public Document getDocument(Node node) {
		if (node.getNodeType() == Node.DOCUMENT_NODE)
			return (Document)node;
		else
			return node.getOwnerDocument();
	}
	
	/**
	 * Create a new element under the xpath
	 * @param xpath The path of the element
	 * @param elementName The name of the element to be created
	 * @return The element created
	 */
	public Node createElement(String xpath, String elementName) {
		try {
			forceElements(root, xpath);
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = null;
			if ((node = nl.nextNode()) != null)
				return node.appendChild(getDocument(root).createElement(elementName));
		} catch (TransformerException te) {
			logger.error("Error occured while creating Element: [" + xpath + "] Name: [" + elementName + "]");
		}
		return null;
	}

	/**
	 * Create a new element under the given node
	 * @param node The node under which to create element
	 * @param elementName The name of the element to be created
	 * @return The element created
	 */
	public Node createElement(Node node, String elementName) {
		return node.appendChild(getDocument(root).createElement(elementName));
	}

	/**
	 * Create a new attribute under the given element
	 * @param xpath The path of the element
	 * @param attrName The name of the attribute to be created
	 * @return The attribute created
	 */
	public Node createAttribute(String xpath, String attrName) {
		try {
			forceElements(root, xpath);
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = null;
			if ((node = nl.nextNode()) != null) {
				Attr attr = getDocument(root).createAttribute(attrName);
				((Element)node).setAttributeNode(attr);
				return attr;
			}
		} catch (TransformerException te) {
			logger.error("Error occured while creating Attribute: [" + xpath + "] Name: [" + attrName + "]");
		}
		return null;
	}

	/**
	 * Create a new attribute under the node
	 * @param node The node under which to create attribute
	 * @param attrName The name of the attribute to be created
	 * @return The attribute created
	 */
	public Node createAttribute(Node node, String attrName) {
		Attr attr = getDocument(root).createAttribute(attrName);
		((Element)node).setAttributeNode(attr);
		return attr;
	}

	/**
	 * Remove the specified element(s)
	 * @param xpath The path of the element(s)
	 */
	public void removeElement(String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = null;
			while ((node = nl.nextNode()) != null)
				node.getParentNode().removeChild(node);
		} catch (TransformerException te) {
			logger.error("Error occured while removing Element: [" + xpath + "]");
		}
	}

	/**
	 * Get the first element's text with the given xpath
	 * @param xpath The path of the element being queried
	 * @return The value of the element
	 */
	public String getElement(String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			if ((node = nl.nextNode()) != null)
				return getNodeText(node);
		} catch (TransformerException te) {
			logger.error("Error occured while getting Element's text: [" + xpath + "]");
		}
		return "";
	}

	/**
	 * Set the first element's text with the given xpath
	 * @param xpath The path of the element being queried
	 * @param text The value of the element
	 */
	public void setElement(String xpath, String text) {
		try {
			forceElements(root, xpath);
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			if ((node = nl.nextNode()) != null) {
				setNodeText(node, text);
				return;
			}
			else
				throw new TransformerException("Can not insert text into the element");
		} catch (TransformerException te) {
			logger.error("Error occured while setting Element's text: [" + xpath + "] Value: [" + text + "]");
		}
	}

	/**
	 * Compose xml_all elements' text delimited by the given character
	 * @param xpath The path of the elements being queried
	 * @param attrName The name of the attribute
	 * @param delimiter The delimiter which split the string 
	 * @return The value of the elements
	 */
	public String getElementAttributeAll(String xpath, String attrName, String delimiter) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			StringBuffer sb = new StringBuffer();
			while ((node = nl.nextNode()) != null) {
				if (sb.length() > 0)
					sb.append(delimiter);
				sb.append(getNodeAttribute(node, attrName));
			}
			return sb.toString();
		} catch (TransformerException te) {
			logger.error("Error occured while getting All Attribute: [" + xpath + "] Name: [" + attrName + "]");
		}
		return "";
	}

	/**
	 * Set xml_all elements' text delimited by the given character
	 * @param xpath The path of the elements being queried belongs to
	 * @param elementName The name of the elements
	 * @param attrName The name of the attribute
	 * @param text The value of the elements
	 * @param delimiter The delimiter which split the string 
	 */
	public void setElementAttributeAll(String xpath, String elementName, String attrName, String text, String delimiter) {
		try {
			forceElements(root, xpath);
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			if ((node = nl.nextNode()) != null) {
				removeAllChildren(node);
				String[] values = text.split(delimiter);
				for (int i = 0; i < values.length; i++) {
					Node nodeChild = node.appendChild(getDocument(root).createElement(elementName));
					setNodeAttribute(nodeChild, attrName, values[i]);
				}
				return;
			}
			else
				throw new TransformerException("Can not insert text into the element");
		} catch (TransformerException te) {
			logger.error("Error occured while setting All Attribute: [" + xpath + "/" + elementName + "] Name: [" + attrName + "] Value: [" + text + "]");
		}
	}

	/**
	 * Get the element's attribute with the given xpath and attribute name
	 * @param xpath The path of the element being queried
	 * @param attrName The name of the attribute
	 * @return The value of the element's attribute
	 */
	public String getElementAttribute(String xpath, String attrName) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			if ((node = nl.nextNode()) != null)
				return getNodeAttribute(node, attrName);
		} catch (TransformerException te) {
			te.printStackTrace();
		}
		return "";
	}

	/**
	 * Set the element's attribute value with the given xpath and attribute name
	 * @param xpath The path of the element being updated
	 * @param attrName The name of the attribute
	 * @param text The value of the element
	 */
	public void setElementAttribute(String xpath, String attrName, String text) {
		try {
			forceElements(root, xpath);
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			if ((node = nl.nextNode()) != null) {
				setNodeAttribute(node, attrName, text);
				return;
			}
			else
				throw new TransformerException("Can not insert text into the element");
		} catch (TransformerException te) {
			logger.error("Error occured while setting Attribute: [" + xpath + "] Name: [" + attrName + "] Value: [" + text + "]");
		}
	}

	/**
	 * Get xml_all child elements of a given node
	 * 
	 * @param xpath
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public ArrayList getMatchNodes(String xpath) {
		ArrayList list = new ArrayList();
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node;
			while ((node = nl.nextNode()) != null) {
				list.add(node);
			}
		}
		catch (TransformerException e) {
			logger.error("Error occured while getting child elements of: [" + xpath + "]");
		}
		return list;
	}
	
	/**
	 * Convert the current XML DOM tree to a string
	 * @return A string containing the content of the DOM tree
	 */
	public String toXML() {
		return toXML(root);
	}

	/**
	 * Convert the given node to an xml string
	 * @param node The node to be converted
	 * @return A string containing the content of the DOM tree
	 */
	public static String toXML(Node node) {
		try {
			StringWriter sw = new StringWriter();
			Transformer serializer = TransformerFactory.newInstance().newTransformer();
			serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
			serializer.setOutputProperty(OutputKeys.INDENT, "yes");
			serializer.transform(new DOMSource(node), new StreamResult(sw));
			return sw.toString();
		}
		catch (TransformerException e) {
			e.printStackTrace();
		}
		return null;
//		StringWriter buffer = new StringWriter();
//		DOMWriter writer = new DOMWriter(buffer, false);
//		writer.print(node);
//		return buffer.toString();
	}

	/**
	 * Get the node indicated by xpath
	 * @param xpath The xpath of the node
	 * @return The new node which is created and inserted
	 */
	public Node getNode(String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = nl.nextNode();
			if (node != null) {
				return node;
			}
		} catch (TransformerException te) {
		}
		return null;
	}
	
	/**
	 * Insert a node before the node indicated by xpath
	 * @param nodeName The name of the inserted node
	 * @param xpath The xpath of the node before which the new node is inserted
	 * @return The new node which is created and inserted
	 */
	public Node insertBefore(String nodeName, String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = nl.nextNode();
			if (node != null) {
				Node nodeInsert = getDocument(root).createElement(nodeName);
				return node.getParentNode().insertBefore(nodeInsert, node);
			}
		} catch (TransformerException te) {
		}
		return null;
	}
	
	/**
	 * Insert a node before the node indicated by the parameter
	 * @param nodeName The name of the inserted node
	 * @param node The node into which the new node is inserted
	 * @return The new node which is created and inserted
	 */
	public Node insertBefore(String nodeName, Node node) {
		if (node != null) {
			Node nodeInsert = getDocument(root).createElement(nodeName);
			return node.getParentNode().insertBefore(nodeInsert, node);
		}
		return null;
	}
	
	/**
	 * Insert a string into the node indicated by xpath
	 * @param xml The string to be inserted
	 * @param xpath The xpath of the node into which the string is inserted
	 */
	public void insertInto(String xml, String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = nl.nextNode();
			if (node != null)
				setNodeText(node, xml);
		} catch (TransformerException te) {
		}
	}
	
	/**
	 * Insert a string into the node specified in parameter
	 * @param xml The string to be inserted
	 * @param node The node into which the string inserted
	 */
	public void insertInto(String xml, Node node) {
		if (node != null)
			setNodeText(node, xml);
	}
	
	/**
	 * Insert a node tree into the node indicated by xpath
	 * @param xmlObj The node tree to be inserted
	 * @param xpath The xpath of the node into which the new nodes are inserted
	 */
	public void insertInto(XMLObject xmlObj, String xpath) {
		try {
			NodeIterator nl = XPathAPI.selectNodeIterator(root, xpath);
			Node node = nl.nextNode();
			if (node != null) {
				Node nodeInsert = getDocument(root).importNode(xmlObj.root, true);
				node.appendChild(nodeInsert);
			}
		} catch (TransformerException te) {
		}
	}
	
	/**
	 * Insert a node tree into a node specified in parameter
	 * @param xmlObj The node tree to be inserted
	 * @param node The node into which the new nodes are inserted
	 */
	public void insertInto(XMLObject xmlObj, Node node) {
		if (node != null) {
			logger.debug("xmlObj: " + (xmlObj == null));
			logger.debug("xmlObj.document: " + (xmlObj.root == null));
			Node nodeInsert = getDocument(root).importNode(xmlObj.root, true);
			node.appendChild(nodeInsert);
		}
	}
	
	/**
	 * Method to ensure that xml_all elements indicated in xpath parameter exist
	 * @param node Indicates the node which xpath is start with
	 * @param xpath The xpath delimited by "/"
	 */
	private void forceElements(Node node, String xpath) throws TransformerException {
		if ((xpath == null) || xpath.equals(""))
			return;
		String[] paths = xpath.split("/");
		StringBuffer path = new StringBuffer();
		Node nodeLastFound = null;
		Node nodeFound = null;
		for (int i = 0; i < paths.length; i++) {
			String strToken = paths[i];
			if ((strToken == null) || strToken.equals("")) {
				path = path.append("/");
				continue;
			}
			path = path.append(strToken);
			NodeIterator nl = XPathAPI.selectNodeIterator(node, path.toString());
			if ((nodeFound = nl.nextNode()) == null) {
				if (nodeLastFound != null)
					node = nodeLastFound;
				Node nodeChild = getDocument(root).createElement(strToken);
				node.appendChild(nodeChild);
				for (int j = i + 1; j < paths.length; j++) {
					node = nodeChild;
					strToken = paths[j];
					nodeChild = getDocument(root).createElement(strToken);
					node.appendChild(nodeChild);
				}
				return;
			}
			else
				nodeLastFound = nodeFound;
			path = path.append("/");
		}
	}
	
	/**
	 * Get the text of the given node
	 * @param node The node to be queried
	 * @return The text contained in the node
	 */
	public String getNodeText(Node node) {
		NodeList nl = node.getChildNodes();
		StringBuffer result = new StringBuffer();
		for (int i = 0; i < nl.getLength(); i++) {
			Node nodeChild = nl.item(i);
			if (isTextNode(nodeChild))
				result.append(nodeChild.getNodeValue());
		}
		return result.toString().trim();
	}
	
	/**
	 * Set the text of the given node
	 * @param node The node to be updated
	 * @param The text contained in the node
	 */
	public void setNodeText(Node node, String text) {
		NodeList nl = node.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++)
			node.removeChild(nl.item(i));
		if (text == null)
			return;
		node.appendChild(getDocument(root).createTextNode(text));
	}
	
	/**
	 * Get the attribute value of the given node
	 * @param node The node to be queried
	 * @param attrName The name of the attribute
	 * @return The attribute value of the node
	 */
	public String getNodeAttribute(Node node, String attrName) {
		NamedNodeMap nodeMap = node.getAttributes();
		Node nodeChild = nodeMap.getNamedItem(attrName);
		if (nodeChild != null)
			return nodeChild.getNodeValue();
		else
			return "";
	}
	
	/**
	 * Set the attribute's value of the given node
	 * @param node The node to be updated
	 * @param attrName The name of the attribute
	 * @param text The attribute's value to be set
	 */
	public void setNodeAttribute(Node node, String attrName, String text) {
		if (text == null)
			return;
		NamedNodeMap nodeMap = node.getAttributes();
		Node nodeChild = nodeMap.getNamedItem(attrName);
		if (nodeChild != null)
			nodeChild.setNodeValue(text);
		else {
			Node nodeAttr = getDocument(root).createAttribute(attrName);
			nodeAttr.setNodeValue(text);
			nodeMap.setNamedItem(nodeAttr);
			//node.appendChild(nodeAttr);
		}
	}

	/**
	 * Remove xml_all the children of the specified node
	 * @param node
	 */
	private void removeAllChildren(Node node) {
		NodeList nl = node.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			node.removeChild(nl.item(i));
		}
	}
}
